Programmation Asynchrone en Dart
Introduction à l'asynchronisme
Dart est un langage à thread unique, mais il permet d'effectuer des opérations asynchrones sans bloquer le thread principal. Cela est essentiel pour :
- Requêtes réseau : Récupérer des données depuis une API
- Opérations de fichiers : Lire/écrire des fichiers
- Opérations de base de données : Requêtes SQL
- Délais et timers : Attendre un certain temps
- Interactions utilisateur : Ne pas bloquer l'interface pendant les traitements
Les Future
Un Future représente le résultat d'une opération asynchrone qui peut ne pas être disponible immédiatement. Il peut être dans l'un des états suivants :
- Incomplet : L'opération n'est pas encore terminée
- Complété avec succès : L'opération est terminée et a produit un résultat
- Complété avec erreur : L'opération a échoué et a produit une erreur
Syntaxe de base
// Future qui retourne une String
Future<String> obtenirDonnees() {
return Future.delayed(
Duration(seconds: 2),
() => "Données récupérées !",
);
}
// Utilisation avec then()
void main() {
print("Début");
obtenirDonnees().then((resultat) {
print(resultat);
});
print("Fin"); // S'exécute avant le résultat du Future
}
Mots-clés async et await
Les mots-clés async et await rendent le code asynchrone plus lisible et plus facile à comprendre.
async
- Déclaration : Utilisé pour déclarer une fonction comme asynchrone
- Effet : La fonction retourne automatiquement un
Future - Utilisation : Permet d'utiliser le mot-clé
awaità l'intérieur de la fonction
Future<String> obtenirDonnees() async {
// Code asynchrone
return "Données";
}
await
- Attente : Suspend l'exécution jusqu'à ce que le
Futuresoit complété - Simplification : Élimine les callbacks imbriqués
- Lisibilité : Le code ressemble à du code synchrone
Future<String> obtenirDonnees() async {
// Simule une opération asynchrone (par exemple, une requête réseau)
await Future.delayed(Duration(seconds: 2));
return "Données récupérées !";
}
void main() async {
print("Début du programme");
String donnees = await obtenirDonnees(); // Attente du résultat du Future
print(donnees);
print("Fin du programme");
}
Règle importante
Le mot-clé await ne peut être utilisé qu'à l'intérieur d'une fonction marquée async.
Gestion des erreurs
Il est crucial de gérer les erreurs potentielles lors des opérations asynchrones.
Avec async/await
Future<String> obtenirDonnees() async {
try {
await Future.delayed(Duration(seconds: 2));
// Simule une erreur
if (DateTime.now().second % 2 == 0) {
throw Exception("Erreur réseau");
}
return "Données récupérées !";
} catch (e) {
print("Erreur : $e");
return "Erreur lors de la récupération des données";
}
}
Avec then() et catchError()
void main() {
obtenirDonnees()
.then((resultat) {
print(resultat);
})
.catchError((erreur) {
print("Erreur capturée : $erreur");
});
}
Bloc finally
Pour exécuter du code que l'opération réussisse ou échoue :
Future<String> obtenirDonnees() async {
try {
await Future.delayed(Duration(seconds: 2));
return "Données récupérées !";
} catch (e) {
print("Erreur : $e");
rethrow; // Relance l'erreur
} finally {
print("Opération terminée"); // Toujours exécuté
}
}
Opérations parallèles
Future.wait()
Permet d'exécuter plusieurs Future en parallèle et d'attendre que tous soient complétés :
Future<void> chargerDonnees() async {
final resultats = await Future.wait([
obtenirUtilisateur(),
obtenirProduits(),
obtenirParametres(),
]);
print("Utilisateur: ${resultats[0]}");
print("Produits: ${resultats[1]}");
print("Paramètres: ${resultats[2]}");
}
Future<String> obtenirUtilisateur() async {
await Future.delayed(Duration(seconds: 1));
return "John Doe";
}
Future<List<String>> obtenirProduits() async {
await Future.delayed(Duration(seconds: 2));
return ["Produit 1", "Produit 2"];
}
Future<Map<String, dynamic>> obtenirParametres() async {
await Future.delayed(Duration(milliseconds: 500));
return {"theme": "dark", "langue": "fr"};
}
Future.any()
Retourne le premier Future qui se complète avec succès :
Future<String> premierServeur() async {
return await Future.any([
obtenirDepuisServeur1(),
obtenirDepuisServeur2(),
obtenirDepuisServeur3(),
]);
}
Timeouts
Limiter le temps d'attente d'une opération asynchrone :
Future<String> obtenirDonneesAvecTimeout() async {
try {
final resultat = await obtenirDonnees().timeout(
Duration(seconds: 5),
onTimeout: () {
throw TimeoutException("La requête a pris trop de temps");
},
);
return resultat;
} on TimeoutException catch (e) {
print("Timeout: $e");
return "Données par défaut";
}
}
Completer
Un Completer permet de créer et contrôler manuellement un Future :
import 'dart:async';
Future<String> operationComplexe() {
final completer = Completer<String>();
// Simule une opération asynchrone
Timer(Duration(seconds: 2), () {
if (DateTime.now().second % 2 == 0) {
completer.complete("Succès");
} else {
completer.completeError("Échec");
}
});
return completer.future;
}
Exemple complet : Chargement de données
class DataService {
Future<UserData> chargerDonneesUtilisateur(int userId) async {
try {
// Étape 1 : Charger les informations de base
print("Chargement du profil...");
final profil = await _chargerProfil(userId);
// Étape 2 : Charger les détails en parallèle
print("Chargement des détails...");
final resultats = await Future.wait([
_chargerCommandes(userId),
_chargerPreferences(userId),
]);
// Étape 3 : Assembler les données
return UserData(
profil: profil,
commandes: resultats[0] as List<String>,
preferences: resultats[1] as Map<String, dynamic>,
);
} catch (e) {
print("Erreur lors du chargement : $e");
rethrow;
}
}
Future<Map<String, String>> _chargerProfil(int userId) async {
await Future.delayed(Duration(seconds: 1));
return {"nom": "John Doe", "email": "john@example.com"};
}
Future<List<String>> _chargerCommandes(int userId) async {
await Future.delayed(Duration(milliseconds: 800));
return ["Commande #123", "Commande #456"];
}
Future<Map<String, dynamic>> _chargerPreferences(int userId) async {
await Future.delayed(Duration(milliseconds: 600));
return {"theme": "dark", "notifications": true};
}
}
class UserData {
final Map<String, String> profil;
final List<String> commandes;
final Map<String, dynamic> preferences;
UserData({
required this.profil,
required this.commandes,
required this.preferences,
});
}
// Utilisation
void main() async {
final service = DataService();
try {
final donnees = await service.chargerDonneesUtilisateur(1);
print("Profil: ${donnees.profil}");
print("Commandes: ${donnees.commandes}");
print("Préférences: ${donnees.preferences}");
} catch (e) {
print("Impossible de charger les données");
}
}
Bonnes pratiques
- Toujours gérer les erreurs : Utilisez
try-catchoucatchError - Éviter les await inutiles : N'attendez que lorsque vous avez besoin du résultat
- Paralléliser quand possible : Utilisez
Future.wait()pour les opérations indépendantes - Utiliser des timeouts : Évitez les attentes infinies
- Retourner des Future typés :
Future<String>plutôt queFuture<dynamic> - Ne pas oublier async : Si vous utilisez
await, la fonction doit êtreasync - Attention à la performance : Trop d'opérations asynchrones peuvent ralentir l'application
Points à retenir
- Future : Représente une valeur future (une seule valeur)
- async : Marque une fonction comme asynchrone
- await : Attend le résultat d'un Future
- try-catch : Gère les erreurs asynchrones
- Future.wait() : Exécute plusieurs Future en parallèle
- timeout() : Limite le temps d'attente
- Completer : Crée des Future personnalisés